# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

import pickle
import re
import os
import job
from collections import defaultdict

class Report(object):
  '''Contains information about a completed job, such as the number of crashes and stack
  traces from every crash. The report is usually displayed on a web page.
  '''
  def __init__(self, job_id):
    self.num_queries = 0
    self.run_time = 0
    self.run_date = 0
    self.job_name = ''
    self.num_crashes = 0
    self.num_row_count_mismatch = 0
    self.num_mismatch = 0
    self.job_id = job_id
    self.git_hash = ''
    self.grouped_results = None
    self.parent_job_name = ''
    self.num_queries_returned_correct_data = 0
    self.get_results()

  @property
  def run_time_str(self):
    '''Return the running time of the job as a string in human readable format.'''
    m, s = divmod(self.run_time, 60)
    h, m = divmod(m, 60)
    return '{0:02d}:{1:02d}:{2:02d}'.format(int(h), int(m), int(s))

  def classify_error(self, error):
    d = {
      ur'LINE \d+:': 'Postgres_error',
      ur'Permission denied': 'permission_denied',
      ur'^AnalysisException': 'AnalysisException',
      ur'^Column \d+ in row \d+ does not match': 'mismatch',
      ur'^Could not connect': 'could_not_connect',
      ur'^IllegalStateException': 'IllegalStateException',
      ur'^Invalid query handle: ': 'invalid_query_handle',
      ur'^Invalid or unknown query handle: ': 'invalid_query_handle',
      ur'^Known issue:': 'known_issue',
      ur'^Operation is in ERROR_STATE': 'error_state',
      ur'^Query timed out after \d+ seconds': 'timeout',
      ur'^Row counts do not match': 'row_counts',
      ur'^Too much data': 'too_much_data',
      ur'^Unknown expr node type: \d+': 'unkown_node',
      ur'^Year is out of valid range': 'year_range',
      ur'^[A-Za-z]+ out of range': 'out_of_range',
      ur'^division by zero': 'division_by_zero'}

    for r in d:
      if re.search(r, error):
        return d[r]
    return 'unrecognized'

  def group_queries(self, all_queries, group_func):
    '''General function that returns a dictionary with keys that are generated by
    group_func. all_queries is a list of queries.
    group_func should take query as a parameter and return a string containing an
    interesting property of the query which will be used as key in the dictionary.
    '''
    grouped_queries = defaultdict(list)
    for query in all_queries:
      grouped_queries[group_func(query)].append(query)
    return grouped_queries

  def __str__(self):
    '''TODO: Render report as text.
    '''
    return ''

  def get_first_impala_frame(self, query_result):
    '''Extracts the first impala frame in the stack trace.
    '''
    stack = query_result['formatted_stack']
    if stack:
      for line in stack.split('\n'):
        match = re.search(ur'(impala::.*) \(', line)
        if match:
          return match.group(1)
    else:
      return None

  def _format_stack(self, stack):
    '''Cleans up the stack trace.
    '''

    def clean_frame(frame):
      #remove memory address from each frame
      reg = re.match(ur'#\d+ *0x[0123456789abcdef]* in (.*)', frame)
      if reg: return reg.group(1)
      # this is for matching lines like "#7  SLL_Next (this=0x9046780, src=0x90467c8...
      reg = re.match(ur'#\d+ *(\S.*)', frame)
      if reg: return reg.group(1)
      return frame

    def stack_gen():
      '''Generator that yields impala stack trace lines line by line.
      '''
      if stack:
        active = False
        for line in stack.split('\n'):
          if active or line.startswith('#0'):
            active = True
            yield line

    return '\n'.join(clean_frame(l) for l in stack_gen())

  def get_results(self):
    '''Analyses the completed job and extracts important results into self. This method
    should be called as soon as the object is created.
    '''
    from controller import PATH_TO_FINISHED_JOBS

    def group_outer_func(query):
      if 'stack' in query:
        return 'stack'
      return self.classify_error(query['error'])

    def stack_group_func(query):
      return self.get_first_impala_frame(query['stack'])

    with open(os.path.join(PATH_TO_FINISHED_JOBS, self.job_id)) as f:
      job = pickle.load(f)
      self.grouped_results = self.group_queries(job.result_list, group_outer_func)

    # Format the stack for queries that have a stack
    for query in self.grouped_results['stack']:
      query['formatted_stack'] = self._format_stack(query['stack'])

    self.num_crashes = len(self.grouped_results['stack'])
    self.num_row_count_mismatch = len(self.grouped_results['row_counts'])
    self.num_mismatch = len(self.grouped_results['mismatch'])

    self.grouped_stacks = self.group_queries(
        self.grouped_results['stack'], self.get_first_impala_frame)

    self.run_time = job.stop_time - job.start_time
    self.run_date = job.start_time
    self.job_name = job.job_name
    self.git_hash = job.git_hash
    self.num_queries_executed = job.num_queries_executed
    self.num_queries_returned_correct_data = job.num_queries_returned_correct_data
    if job.parent_job:
      with open(os.path.join(PATH_TO_FINISHED_JOBS, job.parent_job)) as f:
        parent_job = pickle.load(f)
        self.parent_job_name = parent_job.job_name

  def save_pickle(self):
    from controller import PATH_TO_REPORTS
    with open(os.path.join(PATH_TO_REPORTS, self.job_id), 'w') as f:
      pickle.dump(self, f)
